<?php

/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4 */

/**
 * XML writer
 *
 * PHP version 5
 *
 * LICENSE: This file is part of myLib.
 *
 * myLib is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * myLib is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * @category Writer
 * @package myLib
 * @author Diego Machado de Assis <manejoao@gmail.com>
 * @copyright 2009 Diego Machado de Assis
 * @license http://www.gnu.org/licenses/gpl.html GNU GENERAL PUBLIC LICENSE Version 3
 * @see xhtmloutputter.php
 */


/**
 * XML writer
 *
 * This class implements various methods that simplify the use of the PHP
 * XMLWriter class.
 *
 * @category Writer
 * @package myLib
 * @author Diego Machado de Assis <manejoao@gmail.com>
 * @copyright 2009 Diego Machado de Assis
 * @license http://www.gnu.org/licenses/gpl.html GNU GENERAL PUBLIC LICENSE Version 3
 * @see XHTMLOutputter
 */
class XMLOutputter {

    /**
     * The XMLWriter object
     *
     * Recives the object of the PHP XMLWriter class
     *
     * @var XMLWriter
     * @access private
     */
    private $xmlDoc = null;

    /**
     * Constructor
     *
     * Create the XMLWriter object and set parameters
     *
     * @param string    $output output file to the writer. Defaults to stdout
     * @param boolean   $indent wheter the output should be indented. Default
     *                          true
     *
     * @access public
     */
    public function __construct($output='php://output', $indent=true) {
        if($this->xmlDoc = new XMLWriter()) {
            $this->xmlDoc->openURI($output);
            $this->xmlDoc->setIndent($indent);
        }
    }

    /**
     * Start the XML document
     *
     * Creates the xml declaration for the document
     *
     * @param string    $encoding   The encoding of the document as part of the
     *                              XML declaration. Defaults to 'UTF-8'
     * @param string    $version    The version number of the document as part
     *                              of the XML declaration. Defaults to '1.0'
     *
     * @return boolean  TRUE on success or FALSE on failure
     *
     * @access public
     * @see XMLOutputter::XMLEnd()
     */
    public function XMLStart($encoding='UTF-8', $version='1.0') {
        return $this->xmlDoc->startDocument($version, $encoding);
    }

    /**
     * End the XML document
     *
     * Close the writer and flushes the buffer
     *
     * @return boolean  TRUE on success or FALSE on failure
     *
     * @access public
     * @see XMLOutputter::XMLStart()
     */
    public function XMLEnd() {
        if($this->xmlDoc->endDocument()) {
            $this->xmlDoc->flush();
            return true;
        }
        return false;
    }

    /**
     * Starts a XML element tag
     *
     * Writes to output a element start tag, whith its parameters
     *
     * @param string    $tag        element tag name
     * @param mixed     $attributes array/string of element attributes. If it's
     *                              an array, each element is of the type
     *                              'attribute name' => 'attribute value'. If
     *                              it's a string it will be treated as the
     *                              value of 'id' and 'name' attributes.
     *                              Defaults to NULL
     *
     * @return boolean  TRUE on success or FALSE on failure
     *
     * @access public
     * @see XMLOutputter::elementEnd(), XMLOutputter::element()
     */
    public function elementStart($tag, $attributes=null) {

        if(!$this->xmlDoc->startElement($tag)) {
            return false;
        }

        if(is_array($attributes)) {
            foreach($attributes as $attr_name => $attr_value) {
                if(!$this->xmlDoc->writeAttribute($attr_name, $attr_value)) {
                    return false;
                }
            }
        } else if(is_string($attributes)) {
            if(!$this->xmlDoc->writeAttribute('id', $attributes) &&
                    !$this->xmlDoc->writeAttribute('name', $attributes)) {
                return false;
            }
        } else if(!is_null($attributes)) {
            return false;
        }

        return true;
    }

    /**
     * Ends a XML element tag
     *
     * Should be used in a pair with the 'XMLOutputter::elementStart' method.
     *
     * @param string    $tag    element tag name
     *
     * @return boolean  TRUE on success or FALSE on failure
     *
     * @acess public
     * @see XMLOutputter::elementStart()
     */
    public function elementEnd($tag) {
        $empty_tag = array('base', 'meta', 'link', 'hr', 'br', 'param', 'img',
                'area', 'input', 'col');

        if(in_array($tag, $empty_tag)) {
            if(!$this->xmlDoc->endElement()) {
                return false;
            }
        } else {
            if(!$this->xmlDoc->fullEndElement()) {
                return false;
            }
        }

        return true;
    }

    /**
     * Creates a complete XML element
     *
     * Writes to output a complete XML element (start tag, content (if any) and
     * end tag). Basically, it consist of a call to the methods
     * 'XMLOutputter::elementStart()' (to start the element tag),
     * 'XMLOutputter::text()' (to write the content of the element, escaping
     * special chars like '&', '<' etc.) and 'XMLOutputter::elementEnd()' (to
     * close the element tag).
     *
     * @param string    $tag        element tag name
     * @param mixed     $attributes array/string of element attributes. If it's
     *                              an array, each element is of the type
     *                              'attribute name' => 'attribute value'. If
     *                              it's a string it will be treated as the
     *                              value of 'id' and 'name' attributes.
     *                              Defaults to NULL
     * @param string    $content    element content. Defaults to NULL
     *
     * @return boolean  TRUE on success or FALSE on failure
     *
     * @access public
     * @see XMLOutputter::elementStart(), XMLOutputter::text(),
     *      XMLOutputter::elementEnd()
     */
    public function element($tag, $attributes=null, $content=null) {

        if(!$this->elementStart($tag, $attributes)) {
            return false;
        }
        if(!is_null($content)) {
            if(!$this->text($content)) {
                return false;
            }
        }
        if(!$this->elementEnd($tag)) {
            return false;
        }

        return true;
    }

    /**
     * Writes a full DTD (DOCTYPE) tag
     *
     * The element created is of the form:
     * <!DOCTYPE 'DTD name' PUBLIC "'DTD public identifier'" "'DTD system
     * identifier'">
     *
     * @param string    $name       the DTD name
     * @param string    $publicId   the external subset public identifier
     * @param string    $systemId   the external subset system identifier
     *
     * @return boolean  TRUE on success or FALSE on failure
     *
     * @access public
     */
    public function dtd($name, $publicId, $systemId) {
        return $this->xmlDoc->writeDTD($name, $publicId, $systemId);
    }

    /**
     * Writes a plain text
     *
     * The text written with this method will have his special chars substituted
     * by XML entitites. Example: The character '&' will be rewritten as
     * '&amp;'.
     *
     * @param string    $content    the content of the text
     *
     * @return boolean  TRUE on success or FALSE on failure
     *
     * @access public
     * @see XMLOutputter::raw()
     */
    public function text($content) {
        if(empty($content)) {
            return true;
        }
        if($content && is_string($content)) {
            return $this->xmlDoc->text($content);
        }
        return false;
    }

    /**
     * Writes a raw XML text
     *
     * Writes the text content to output with no escapes (the text is written
     * verbatim).
     *
     * @param string    $content    text string to write
     *
     * @return boolean  TRUE on success or FALSE on failure
     *
     * @access public
     * @see XMLOutputter::text()
     */
    public function raw($content) {
        if(empty($content)) {
            return true;
        }
        if($content && is_string($content)) {
            return $this->xmlDoc->writeRaw($content);
        }
        return false;
    }

    /**
     * Writes a comment
     *
     * @param string    $content    the content of the comment
     *
     * @return boolean  TRUE on success or FALSE on failure
     *
     * @access public
     */
    public function comment($content) {
        if(empty($content)) {
            return true;
        }
        if($content && is_string($content)) {
            return $this->xmlDoc->writeComment($content);
        }
        return false;
    }

    /**
     * Writes a full CDATA tag
     *
     * Writing Javascript code inside a valid XHTML documet must be donne inside
     * a CDATA region because using the characters '<', '>', '&' and '"' as
     * characters rather than entities will not validate, and Javascript would
     * not understand it if you coded something as 'if (A &gt; B){}'. However,
     * not all browsers support XHTML and therefore may not understand the tags
     * '<![CDATA[' and ']]>', so hiding the CDATA from non-XHTML browsers with
     * the single line javascript comment of '//', allowes XHTML compliant
     * browsers to except special characters within the CDATA, and non-compliant
     * browsers simply ignore the CDATA call since it’s part of a comment.
     *
     * @param string    $content    the content of the CDATA
     * @param boolean   $commented  if the CDATA begin and end tags must be
     *                              preceded by a Javascript single line comment
     *                              '//'
     *
     * @return boolean  TRUE on success or FALSE on failure
     *
     * @access public
     */
    public function cdata($content, $commented = true) {
        if(empty($content)) {
            return true;
        }
        if($content && is_string($content)) {
            if($commented) {
                return ($this->raw('//<![CDATA['."\n") &&
                        $this->raw($content) &&
                        $this->raw("\n".'//]]>'));
            }
            return $this->xmlDoc->writeCData($content);
        }
        return false;
    }

}
